【SwiftUI】Floating Buttonの実装をしてみたのでメモ(TextFieldの入力チェック付き)
概要
大阪オフィスの山田です。今回、SwiftUIでFloating Buttonを趣味アプリで実装してみたので、やり方メモを公開します。
環境
- Xcode: 11.5
- macOS: 10.15.4
Floating Buttonを作る
このようなボタンを実装します。
以下の実装でFloating Buttonの外観を作ります。
struct FloatingButton: View { var body: some View { VStack { // --- 1 Spacer() HStack { // --- 2 Spacer() Button(action: { print("Tapped!!") // --- 3 }, label: { Image(systemName: "pencil") .foregroundColor(.white) .font(.system(size: 24)) // --- 4 }) .frame(width: 60, height: 60) .background(Color.orange) .cornerRadius(30.0) .shadow(color: .gray, radius: 3, x: 3, y: 3) .padding(EdgeInsets(top: 0, leading: 0, bottom: 16.0, trailing: 16.0)) // --- 5 } } } }
プログラムを解説します。
- 1, 2: ボタンが右下に配置されるよう、
VStack
とHStack
とSpacer
を使用 - 3: ボタンをタップした時のアクション
- 現時点ではまだ、文字を出力するだけの処理。ここは後ほど実装します。
- 4: ボタンのラベル部分を作成
- 今回はシステムで用意されている画像を使って、色を白色にしています。そのままでは少し小さいので
font
メソッドを使って、大きくしています。
- 今回はシステムで用意されている画像を使って、色を白色にしています。そのままでは少し小さいので
- 5: ボタン全体のデザイン
- 大きさは固定値指定、
cornerRadius
も固定値指定にして丸くしています。background
で背景色を設定しています。padding
で、ボタンの下と右にスペースを確保しています。このメソッドを省くと、画面右下にぴったりくっつきます。そして、最後にshadow
で影をつけています。
- 大きさは固定値指定、
これでFloating Buttonの外観ができたので、実際に、浮いたボタンになっているか確認します。
Floating Buttonをパーツとして表示する
実際にFloating Buttonを浮いたように表示するにはZStack
を使います。これを使うことでViewを重ね合わせたような外観を実装することができます。
struct InputView: View { var body: some View { ZStack { VStack { Spacer() HStack() { Spacer() Text("Yattane!") .font(.title) } } FloatingButton() } } }
実装したInputView
を表示すると、以下の画像のようになります。Text
の上にボタンが被さっているのがわかります。
TextFieldでキーボードが表示された時に隠れないようにする
この実装をするには、キーボードが表示された時にキーボードの高さを取得、通知するクラスが必要です。SwiftUIでキーボードで文字が隠れないように処理をいれる: Qiitaや、KeyboardObserving: GitHubの実装が参考になります。Notification Centerを使ってキーボードの高さを取得します。今回はキーボード領域が被った分だけずらす、のような複雑なことはせず、単にキーボードが表示されたら高さを、非表示になれば0を設定するようにしています。
class KeyboardService: ObservableObject { /// キーボードの高さを伝えるプロパティ @Published var keyboardHeight: CGFloat = 0.0 let defaultNotification = NotificationCenter.default /// キーボードの監視開始 func start() { defaultNotification.addObserver( self, selector: #selector(self.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) defaultNotification.addObserver( self, selector: #selector(self.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } /// キーボードの監視終了 func end() { NotificationCenter .default .removeObserver( self, name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter .default .removeObserver( self, name: UIResponder.keyboardWillHideNotification, object: nil) } /// キーボードが表示されたら、高さをkeyboardHeighに設定する @objc func keyboardWillShow(_ notification: Notification) { guard let rect = (notification .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return } self.keyboardHeight = rect.size.height } /// キーボードが表示されたら、高さを0に設定する @objc func keyboardWillHide(_ notification: Notification) { self.keyboardHeight = 0 } }
これで、キーボードの高さを取得するクラスが実装できました。次にViewに繋ぎこみます。
struct InputView: View { @State var inputText = "" @ObservedObject var keyboard = KeyboardService() // --- 1 var body: some View { ZStack { VStack { TextField( "入力してね", text: $inputText, onEditingChanged: { editing in }).padding() // --- 5 } FloatingButton(bottomPadding: self.keyboard.keyboardHeight) // --- 2 }.onAppear { self.keyboard.start() // --- 3 }.onDisappear { self.keyboard.end() // --- 4 } } }
- 1:
ObservedObject
としてKeyboardService
を宣言 - 2:
KeyboardService
を使ってFloatingButton
にキーボードの高さを伝えるFloatingButton
クラスは後ほど修正します。
- 3, 4: Appear時、DisAppear時にそれぞれ、キーボードの監視を開始、終了
- 5: 入力用の
TextField
を用意
続いて、FloatingButton
の実装に進みます。
struct FloatingButton: View { var bottomPadding: CGFloat = 0 // --- 1 var body: some View { VStack { Spacer() HStack { Spacer() Button(action: { print("Tapped!!") }, label: { Image(systemName: "pencil") .foregroundColor(.white) .font(.system(size: 24)) }) .frame(width: 60, height: 60) .background(Color.orange) .cornerRadius(30.0) .shadow(color: .gray, radius: 3, x: 3, y: 3) .padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0)) // --- 2 .animation(.easeOut) } } } }
- 1: ボタン下側のpaddingを保持する変数を宣言
- 2: paddingの適用
キーボードが表示された時、高さが伝わり、paddingの値を変更する、という仕組みになっています。
実際に動かすと以下のようになります。
ボタンタップ時の動きを生成時に指定する
ボタンタップ時に入力された文字を画面に表示するようにします。それでは実装していきます。まず、ボタンタップで入力された画面を表示します。
struct InputView: View { @State var inputText = "" @State var outputText = "" // --- 1 @ObservedObject var keyboard = KeyboardService() var body: some View { ZStack { VStack { Text(outputText) // --- 2 TextField( "入力してね", text: $inputText, onEditingChanged: { editing in }).padding() } FloatingButton( bottomPadding: self.keyboard.keyboardHeight, tappedHandler: self.showText // --- 3 ) }.onAppear { self.keyboard.start() }.onDisappear { self.keyboard.end() } } // --- 4 func showText() { outputText = inputText } } struct FloatingButton: View { var bottomPadding: CGFloat = 0 var tappedHandler: (() -> Void)? = nil // --- 5 var body: some View { VStack { Spacer() HStack { Spacer() Button(action: { self.tappedHandler?() // --- 6 }, label: { Image(systemName: "pencil") .foregroundColor(.white) .font(.system(size: 24)) }) .frame(width: 60, height: 60) .background(Color.orange) .cornerRadius(30.0) .shadow(color: .gray, radius: 3, x: 3, y: 3) .padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0)) .animation(.easeOut) } } } }
まず、InputView
の修正です。
- 1: 表示する文字列を格納する変数を用意
- 2: 変数outputTextをTextで表示
- 3: ボタンをタップした時に動作させる内容を指定
- ここでは4にてメソッドを定義して、それを呼び出すようにしています。
続いてFloatingButton
です。
- 5: タップ時に動作させるハンドラーを変数として定義
- 6: タップ時に呼び出すようにしています。
実際の動きはこちらになります。
TextFieldの入力状況でdisabledを制御する
次は、1文字も入力されていない時には、ボタンを押せないようにします。
struct InputView: View { @State var inputText = "" @State var outputText = "" @ObservedObject var keyboard = KeyboardService() var body: some View { ZStack { VStack { Text(outputText) TextField( "入力してね", text: $inputText, onEditingChanged: { editing in }).padding() } FloatingButton( bottomPadding: self.keyboard.keyboardHeight, tappedHandler: self.showText, isDisabled: inputText.isEmpty // --- 1 ) }.onAppear { self.keyboard.start() }.onDisappear { self.keyboard.end() } } func showText() { outputText = inputText } } struct FloatingButton: View { var bottomPadding: CGFloat = 0 var tappedHandler: (() -> Void)? = nil var isDisabled: Bool = false // --- 2 var body: some View { VStack { Spacer() HStack { Spacer() Button(action: { self.tappedHandler?() }, label: { Image(systemName: "pencil") .foregroundColor(.white) .font(.system(size: 24)) }) .frame(width: 60, height: 60) .background(backgroundColor) // --- 3 .cornerRadius(30.0) .shadow(color: .gray, radius: 3, x: 3, y: 3) .padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding + 16.0, trailing: 16.0)) .animation(.easeOut) .disabled(isDisabled) // --- 4 } } } // --- 5 var backgroundColor: Color { return isDisabled ? Color.gray : Color.orange } }
- 1, 2:
inputText
が空だった場合は、isDisabled
変数をtrue
にする。そうでなければfalse
- 4: 変数
isDisabled
の値でdisableにするか否かを決定 - 3, 5: disableの時はグレーアウトするように背景色を変更
実際の動きはこちらになります。
おわりに
SwiftUIおもしろー。OSSになったら良いのになぁ。